-- Copyright (c) 2015 Shunsuke Shimizu (grafi)
-- Copyright (c) 2015,2016 Lucky Byte, Inc.
--
-- https://github.com/grafi-tt/lunajson
--
--[[
The MIT License (MIT)

Copyright (c) 2015 Shunsuke Shimizu (grafi)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]

local error = error
local byte, find, format, gsub, match = string.byte, string.find, string.format,  string.gsub, string.match
local concat = table.concat
local tostring = tostring
local pairs, type = pairs, type
local setmetatable = setmetatable
local huge, tiny = 1/0, -1/0

local f_string_pat
if _VERSION == "Lua 5.1" then
    -- use the cluttered pattern because lua 5.1 does not handle \0 in a pattern correctly
    f_string_pat = '[^ -!#-[%]^-\255]'
else
    f_string_pat = '[\0-\31"\\]'
end

local _ENV = nil

local function newencoder()
    local v, nullv
    local i, builder, visited

    local function f_tostring(v)
        builder[i] = tostring(v)
        i = i+1
    end

    local radixmark = match(tostring(0.5), '[^0-9]')
    local delimmark = match(tostring(12345.12345), '[^0-9' .. radixmark .. ']')
    if radixmark == '.' then
        radixmark = nil
    end

    local radixordelim
    if radixmark or delimmark then
        radixordelim = true
        if radixmark and find(radixmark, '%W') then
            radixmark = '%' .. radixmark
        end
        if delimmark and find(delimmark, '%W') then
            delimmark = '%' .. delimmark
        end
    end

    local f_number = function(n)
        if tiny < n and n < huge then
            local s = format("%.17g", n)
            if radixordelim then
                if delimmark then
                    s = gsub(s, delimmark, '')
                end
                if radixmark then
                    s = gsub(s, radixmark, '.')
                end
            end
            builder[i] = s
            i = i+1
            return
        end
        error('invalid number')
    end

    local doencode

    local f_string_subst = {
        ['"'] = '\\"',
        ['\\'] = '\\\\',
        ['\b'] = '\\b',
        ['\f'] = '\\f',
        ['\n'] = '\\n',
        ['\r'] = '\\r',
        ['\t'] = '\\t',
        __index = function(_, c)
            return format('\\u00%02X', byte(c))
        end
    }
    setmetatable(f_string_subst, f_string_subst)

    local function f_string(s)
        builder[i] = '"'
        if find(s, f_string_pat) then
            s = gsub(s, f_string_pat, f_string_subst)
        end
        builder[i+1] = s
        builder[i+2] = '"'
        i = i+3
    end

    local function f_table(o)
        if visited[o] then
            error("loop detected")
        end
        visited[o] = true

        local tmp = o[0]
        if type(tmp) == 'number' then -- arraylen available
            builder[i] = '['
            i = i+1
            for j = 1, tmp do
                doencode(o[j])
                builder[i] = ','
                i = i+1
            end
            if tmp > 0 then
                i = i-1
            end
            builder[i] = ']'

        else
            tmp = o[1]
            if tmp ~= nil then -- detected as array
                builder[i] = '['
                i = i+1
                local j = 2
                repeat
                    doencode(tmp)
                    tmp = o[j]
                    if tmp == nil then
                        break
                    end
                    j = j+1
                    builder[i] = ','
                    i = i+1
                until false
                builder[i] = ']'

            else -- detected as object
                builder[i] = '{'
                i = i+1
                local tmp = i
                for k, v in pairs(o) do
                    if type(k) ~= 'string' then
                        error("non-string key")
                    end
                    f_string(k)
                    builder[i] = ':'
                    i = i+1
                    doencode(v)
                    builder[i] = ','
                    i = i+1
                end
                if i > tmp then
                    i = i-1
                end
                builder[i] = '}'
            end
        end

        i = i+1
        visited[o] = nil
    end

    local dispatcher = {
        boolean = f_tostring,
        number = f_number,
        string = f_string,
        table = f_table,
        __index = function()
            error("invalid type value")
        end
    }
    setmetatable(dispatcher, dispatcher)

    function doencode(v)
        if v == nullv then
            builder[i] = 'null'
            i = i+1
            return
        end
        return dispatcher[type(v)](v)
    end

    local function encode(v_, nullv_)
        v, nullv = v_, nullv_
        i, builder, visited = 1, {}, {}

        doencode(v)
        return concat(builder)
    end

    return encode
end

return newencoder
